acme.sh: add `abort` service command and improve interactive messages
authorAditya Bhargava <[email protected]>
Mon, 29 Sep 2025 22:05:19 +0000 (18:05 -0400)
committerToke Høiland-Jørgensen <[email protected]>
Wed, 8 Oct 2025 18:27:32 +0000 (20:27 +0200)
For runs started interactively, improve messaging and allow a run to be
aborted with `service acme abort`.

Signed-off-by: Aditya Bhargava <[email protected]>
net/acme-acmesh/files/hook.sh
net/acme-common/files/acme.init

index 0bca1f19aa25fa3bb01c27d8143e6f6f9dc4aeca..1b6a04feaafbecdc67383ca20d4a28f1340ca6d2 100644 (file)
@@ -39,6 +39,53 @@ link_certs() {
        fi
 }
 
+handle_signal() {
+       local notify_op=$1
+       local label_op=$2
+       wait_notify() {
+               # wait for acme.sh child job to die, *then* notify about status
+               wait
+               log warn "$label_op aborted: $main_domain"
+               $NOTIFY "${notify_op}-failed"
+               exit 1
+       }
+
+       trap wait_notify TERM
+       # try to kill the cgroup
+       local cgroup=$(cut -d : -f 3 /proc/$$/cgroup)
+       if [[ "$cgroup" == '/services/acme/*' ]]; then
+               # send SIGTERM to all processes in this process's cgroup. this
+               # relies on procd's having set up the cgroup for the instance.
+               read -r -d '' pids < /sys/fs/cgroup${cgroup}/cgroup.procs 
+               kill -TERM $pids 2> /dev/null
+       fi
+
+       # if we're here, either the cgroup wasn't as exected to be set up by
+       # procd or killing the cgroup PIDs failed. try to kill the process
+       # group, assuming this process is the group leader. this is actually
+       # unlikely since procd doesn't set service PGIDs (so they aren't group
+       # leaders).
+       kill -TERM -$$ 2> /dev/null
+
+       # if we're here, cgroup-based killing was avoided or didn't work and
+       # PGID-based killing didn't work. fall back to the raciest option.
+       trap "" TERM
+       term_descendants() {
+               local pids=$@
+               local pid=
+               # `pgrep -P` returns nothing if given a non-existent PID
+               # (even if the PID has live children), so children must
+               # be killed first
+               for pid in $pids; do
+                       term_descendants $(pgrep -P "$pid")
+                       kill -TERM "$pid" 2> /dev/null
+               done
+       }
+       term_descendants $(jobs -p)
+
+       wait_notify
+}
+
 case $1 in
 get)
        set --
@@ -67,10 +114,11 @@ get)
                else
                        set -- "$@" --renew --home "$state_dir" -d "$main_domain"
                        log info "$ACME $*"
-                       trap 'log err "Renew failed: SIGINT";$NOTIFY renew-failed;exit 1' INT
-                       $ACME "$@"
+                       trap "handle_signal renew Renewal" INT TERM
+                       $ACME "$@" &
+                       wait $!
                        status=$?
-                       trap - INT
+                       trap - INT TERM
 
                        case $status in
                        0)
@@ -141,12 +189,13 @@ get)
        set -- "$@" --issue --home "$state_dir"
 
        log info "$ACME $*"
-       trap 'log err "Issue failed: SIGINT";$NOTIFY issue-failed;exit 1' INT
+       trap "handle_signal issue Issuance" INT TERM
        "$ACME" "$@" \
                --pre-hook "$NOTIFY prepare" \
-               --renew-hook "$NOTIFY renewed"
+               --renew-hook "$NOTIFY renewed" &
+       wait $!
        status=$?
-       trap - INT
+       trap - INT TERM
 
        case $status in
        0)
index 5d441f2325385033a2e7054f926f152cead09a8c..cb2b5505d2391e6e446c80e0186b0e46f904c2e6 100644 (file)
@@ -13,7 +13,8 @@ LOG_TAG=acme
 # shellcheck source=net/acme/files/functions.sh
 . "$IPKG_INSTROOT/usr/lib/acme/functions.sh"
 
-extra_command "renew" "Start a certificate renew"
+extra_command "abort" "Abort running certificate issuances/renewals"
+extra_command "renew" "Run certificate issuances/renewals"
 
 delete_nft_rule() {
        if [ "$NFT_HANDLE" ]; then
@@ -136,6 +137,7 @@ get_cert() {
        load_options "$section"
 
        load_credentials() {
+               # use `eval` to correctly strip quotes around credential values
                eval procd_append_param env "$1"
        }
        config_list_foreach "$section" credentials load_credentials
@@ -175,15 +177,20 @@ start_service() {
 }
 
 service_started() {
-       echo "Certificate renewal enabled via cron. To renew now, run '/etc/init.d/acme renew'."
+       echo 'Nightly certificate renewal enabled. To renew now, run `service acme renew`.'
 }
 
 stop_service() {
        sed -i '\|/etc/init.d/acme|d' /etc/crontabs/root
+       running && stop_aborted="Running certificate renewal(s) aborted and a"
 }
 
 service_stopped() {
-       echo "Certificate renewal is disabled."
+       if enabled; then
+               untilboot=' until next boot. To disable permanently, run `service acme disable`'
+       fi
+       echo "${stop_aborted:-A}utomatic nightly renewal disabled$untilboot."
+       echo 'To re-enable nightly renewal, run `service acme start`. To issue/renew now, run `service acme renew`.'
 }
 
 service_triggers() {
@@ -201,5 +208,21 @@ load_and_run() {
 }
 
 renew() {
+       echo "Starting certificate issuance/renewal in the background; see system log for progress."
+       echo 'Issuances/renewals can be aborted with `service acme abort`.'
        rc_procd load_and_run
 }
+
+abort() {
+       procd_lock
+       if running "$@"; then
+               procd_kill "$(basename ${basescript:-$initscript})" "$1"
+               echo "Aborting certificate issuance(s)/renewal(s); see system log for confirmation."
+       elif [ -z "$1" ]; then
+               echo "No certificate issuances/renewals running to abort!"
+               exit 1
+       else
+               echo "No certificate issuance/renewal \"$1\" running to abort!"
+               exit 1
+       fi
+}